/* Copyright (c) 2001-2011, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb.lib;

import java.util.Enumeration;
import java.util.logging.Level;
import java.util.logging.ConsoleHandler;
import java.util.logging.Logger;
import java.util.logging.LogManager;
import java.util.Map;
import java.io.File;
import java.io.FileInputStream;
import java.util.Properties;
import java.io.IOException;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.lang.reflect.Method;

/**
 * A logging framework wrapper that supports java.util.logging and log4j.
 * <P/>
 * Logger hierarchies are stored at the Class level.
 * Log4j will be used if the Log4j system (not necessarily config files) are
 * found in the runtime classpath.
 * Otherwise, java.util.logging will be used.
 * <P/>
 * This is pretty safe because for use cases where multiple hierarchies
 * are desired, classloader hierarchies will effectively isolate multiple
 * class-level Logger hierarchies.
 * <P/>
 * Sad as it is, the java.util.logging facility lacks the most basic
 * developer-side and configuration-side capabilities.
 * Besides having a non-scalable discovery system, the designers didn't
 * comprehend the need for a level between WARNING and SEVERE!
 * Since we don't want to require log4j in Classpath, we have to live
 * with these constraints.
 * <P/>
 * As with all the popular logging frameworks, if you want to capture a
 * stack trace, you must use the two-parameters logging methods.
 * I.e., you must also pass a String, or only toString() from your
 * throwable will be captured.
 * <P/>
 * Usage example:<CODE><PRE>
 * private static FrameworkLogger logger =
 *        FrameworkLogger.getLog(SqlTool.class);
 * ...
 *   logger.finer("Doing something log-worthy");
 * </PRE> </CODE>
 *
 * @author Blaine Simpson (blaine dot simpson at admc dot com)
 * @version 2.0.1
 * @since 1.9.0
 */
public class FrameworkLogger {
    /**
     * Utility method for integrators.
     * Returns a string representation of the active Logger instance keys.
     * <p>
     * Not named similar to 'toString' to avoid ambiguity with instance method
     * toString.
     * </p>
     */
    public static String report() {
        return new StringBuilder().append(loggerInstances.size())
                .append(" logger instances:  ").append(loggerInstances.keySet())
                .toString();
    }

    static private Map     loggerInstances  = new HashMap();
    static private Map     jdkToLog4jLevels = new HashMap();
    static private Method  log4jGetLogger;
    static private Method  log4jLogMethod;
    private Object         log4jLogger;
    private Logger         jdkLogger;
    static private boolean haveLoadedOurDefault;
    static private ConsoleHandler  consoleHandler = new ConsoleHandler();
    // No need for more than one static, since we have only one console

    static {
        String propVal = System.getProperty("hsqldb.reconfig_logging");
        if (propVal == null || !propVal.equalsIgnoreCase("false")) {
            reconfigure();
        }
    }

    /**
     * Frees Logger(s), if any, with the specified category, or that begins with
     * the specified prefix + dot.
     * <p>
     * Note that as of today, this depends on the underlying logging framework
     * implementation to release the underlying Logger instances.
     * JUL in Sun's JVM uses weak references, so that should be fine.
     * Log4j as of today seems to use strong references (and no API hooks to
     * free anything), so this method will probably have little benefit for
     * Log4j.
     * </p>
     */
    public static synchronized void clearLoggers(String prefixToZap) {
        Set targetKeys = new HashSet();
        java.util.Iterator it = loggerInstances.keySet().iterator();
        String k;
        String dottedPrefix = prefixToZap + '.';
        while (it.hasNext()) {
            k = (String) it.next();
            if (k.equals(prefixToZap) || k.startsWith(dottedPrefix)) {
                targetKeys.add(k);
            }
        }
        loggerInstances.keySet().removeAll(targetKeys);
    }

    static void reconfigure() {
        Class log4jLoggerClass = null;
        loggerInstances.clear();

        try {
            log4jLoggerClass = Class.forName("org.apache.log4j.Logger");
        } catch (Exception e) {
            log4jLoggerClass = null;
        }

        if (log4jLoggerClass == null) try {
            log4jGetLogger = null;
            log4jLogMethod = null;
            LogManager lm = LogManager.getLogManager();
            if (haveLoadedOurDefault || isDefaultJdkConfig()) {
                haveLoadedOurDefault = true;
                consoleHandler.setFormatter(
                        new BasicTextJdkLogFormatter(false));
                consoleHandler.setLevel(Level.INFO);
                lm.readConfiguration(
                        FrameworkLogger.class.getResourceAsStream(
                        "/org/hsqldb/resources/jdklogging-default.properties"));
                Logger cmdlineLogger = Logger.getLogger("org.hsqldb.cmdline");
                cmdlineLogger.addHandler(consoleHandler);
                cmdlineLogger.setUseParentHandlers(false);
            } else {
                // Do not intervene.  Use JDK logging exactly as configured by
                // user.
                lm.readConfiguration();
                // The only bad thing about doing this is that if the app has
                // programmatically changed the logging config after starting
                // the program but before using FrameworkLogger, we will
                // clobber those customizations.
            }
        } catch (Exception e) {
            throw new RuntimeException(
                "<clinit> failure initializing JDK logging system", e);
        } else try {
            Method log4jToLevel = Class.forName(
                "org.apache.log4j.Level").getMethod(
                "toLevel", new Class[]{ String.class });

            jdkToLog4jLevels.put(Level.ALL,
                                 log4jToLevel.invoke(null,
                                     new Object[]{ "ALL" }));
            jdkToLog4jLevels.put(Level.FINER,
                                 log4jToLevel.invoke(null,
                                     new Object[]{ "DEBUG" }));
            jdkToLog4jLevels.put(Level.WARNING,
                                 log4jToLevel.invoke(null,
                                     new Object[]{ "ERROR" }));
            jdkToLog4jLevels.put(Level.SEVERE,
                                 log4jToLevel.invoke(null,
                                     new Object[]{ "FATAL" }));
            jdkToLog4jLevels.put(Level.INFO,
                                 log4jToLevel.invoke(null,
                                     new Object[]{ "INFO" }));
            jdkToLog4jLevels.put(Level.OFF,
                                 log4jToLevel.invoke(null,
                                     new Object[]{ "OFF" }));
            jdkToLog4jLevels.put(Level.FINEST,
                                 log4jToLevel.invoke(null,
                                     new Object[]{ "TRACE" }));
            jdkToLog4jLevels.put(Level.WARNING,
                                 log4jToLevel.invoke(null,
                                     new Object[]{ "WARN" }));

            log4jLogMethod = log4jLoggerClass.getMethod("log",
                    new Class[] {
                String.class, Class.forName("org.apache.log4j.Priority"),
                Object.class, Throwable.class
            });

            log4jGetLogger = log4jLoggerClass.getMethod("getLogger",
                    new Class[]{ String.class });

            // This last object is what we toggle on to generate either
            // Log4j or Jdk Logger objects (to wrap).
        } catch (Exception e) {
            throw new RuntimeException(
                "<clinit> failure instantiating present Log4j system", e);
        }
    }

    /**
     * User may not use the constructor.
     */
    private FrameworkLogger(String s) {

        if (log4jGetLogger == null) {
            jdkLogger = Logger.getLogger(s);
        } else {
            try {
                log4jLogger = log4jGetLogger.invoke(null, new Object[]{ s });
            } catch (Exception e) {
                throw new RuntimeException(
                    "Failed to instantiate Log4j Logger", e);
            }
        }

        loggerInstances.put(s, this);
    }

    /**
     * User's entry-point into this logging system.
     * <P/>
     * You normally want to work with static (class-level) pointers to
     * logger instances, for performance efficiency.
     * See the class-level JavaDoc for a usage example.
     *
     * @see FrameworkLogger
     */
    public static FrameworkLogger getLog(Class c) {
        return getLog(c.getName());
    }

    /**
     * This method just defers to the getLog(Class) method unless default
     * (no local configuration) JDK logging is being used;
     * In that case, this method assures that the returned logger has an
     * associated FileHander using the supplied String identifier.
     */
    public static FrameworkLogger getLog(Class c, String contextId) {
        return (contextId == null)
                ?  getLog(c)
                :  getLog(contextId + '.' + c.getName());
    }

    /**
     * This method just defers to the getLog(String) method unless default
     * (no local configuration) JDK logging is being used;
     * In that case, this method assures that the returned logger has an
     * associated FileHander using the supplied String identifier.
     */
    public static FrameworkLogger getLog(String baseId, String contextId) {
        return (contextId == null)
                ?  getLog(baseId)
                :  getLog(contextId + '.' + baseId);
    }

    /**
     * Alternative entry-point into this logging sytem, for cases where
     * you want to share a single logger instance among multiple classes,
     * or you want to use multiple logger instances from a single class.
     *
     * @see #getLog(Class)
     */
    public static FrameworkLogger getLog(String s) {

        if (loggerInstances.containsKey(s)) {
            return (FrameworkLogger) loggerInstances.get(s);
        }

        return new FrameworkLogger(s);
    }

    /**
     * Just like FrameworkLogger.log(Level, String),
     * but also logs a stack trace.
     *
     * @param level java.util.logging.Level level to filter and log at
     * @param message Message to be logged
     * @param t Throwable whose stack trace will be logged.
     * @see #log(Level, String)
     * @see Logger#log(Level, String)
     * @see Level
     */
    public void log(Level level, String message, Throwable t) {
        privlog(level, message, t, 2, FrameworkLogger.class);
    }

    /**
     * The "priv" prefix is historical.
     * This is for special usage when you need to modify the reported call
     * stack.
     * If you don't know that you want to do this, then you should not use
     * this method.
     */
    public void privlog(Level level, String message,
    Throwable t, int revertMethods, Class skipClass) {

        if (log4jLogger == null) {
            StackTraceElement elements[] = new Throwable().getStackTrace();
            String            c = elements[revertMethods].getClassName();
            String            m = elements[revertMethods].getMethodName();

            if (t == null) {
                jdkLogger.logp(level, c, m, message);
            } else {
                jdkLogger.logp(level, c, m, message, t);
            }
        } else {
            try {
                log4jLogMethod.invoke(log4jLogger, new Object[] {
                    skipClass.getName(), jdkToLog4jLevels.get(level), message,
                    t
                });
            } catch (Exception e) {
                throw new RuntimeException(
                    "Logging failed when attempting to log: " + message, e);
            }
        }
    }

    public void enduserlog(Level level, String message) {
        /* This method is SqlTool-specific, which is where this class began at.
         * Need to move this back there, but it needs access to the logging
         * structures private to this class.  Thinking...
         */

        if (log4jLogger == null) {
            String c = FrameworkLogger.class.getName();
            String m = "\\l";

            jdkLogger.logp(level, c, m, message);
        } else {
            try {
                log4jLogMethod.invoke(log4jLogger, new Object[] {
                    FrameworkLogger.class.getName(),
                    jdkToLog4jLevels.get(level),
                    message, null
                });

                // Test where SqlFile correct here.
            } catch (Exception e) {
                throw new RuntimeException(
                    "Logging failed when attempting to log: " + message, e);
            }
        }
    }

    // Wrappers

    /**
     * @param level java.util.logging.Level level to filter and log at
     * @param message Message to be logged
     * @see Logger#log(Level, String)
     * @see Level
     */
    public void log(Level level, String message) {
        privlog(level, message, null, 2, FrameworkLogger.class);
    }

    /**
     * @param message Message to be logged
     * @see Logger#finer(String)
     */
    public void finer(String message) {
        privlog(Level.FINER, message, null, 2, FrameworkLogger.class);
    }

    /**
     * @param message Message to be logged
     * @see Logger#warning(String)
     */
    public void warning(String message) {
        privlog(Level.WARNING, message, null, 2, FrameworkLogger.class);
    }

    /**
     * @param message Message to be logged
     * @see Logger#severe(String)
     */
    public void severe(String message) {
        privlog(Level.SEVERE, message, null, 2, FrameworkLogger.class);
    }

    /**
     * @param message Message to be logged
     * @see Logger#info(String)
     */
    public void info(String message) {
        privlog(Level.INFO, message, null, 2, FrameworkLogger.class);
    }

    /**
     * @param message Message to be logged
     * @see Logger#finest(String)
     */
    public void finest(String message) {
        privlog(Level.FINEST, message, null, 2, FrameworkLogger.class);
    }

    /**
     * This is just a wrapper for FrameworkLogger.warning(), because
     * java.util.logging lacks a method for this critical purpose.
     *
     * @param message Message to be logged
     * @see #warning(String)
     */
    public void error(String message) {
        privlog(Level.WARNING, message, null, 2, FrameworkLogger.class);
    }

    /**
     * Just like FrameworkLogger.finer(String), but also logs a stack trace.
     *
     * @param t Throwable whose stack trace will be logged.
     * @see #finer(String)
     */
    public void finer(String message, Throwable t) {
        privlog(Level.FINER, message, t, 2, FrameworkLogger.class);
    }

    /**
     * Just like FrameworkLogger.warning(String), but also logs a stack trace.
     *
     * @param t Throwable whose stack trace will be logged.
     * @see #warning(String)
     */
    public void warning(String message, Throwable t) {
        privlog(Level.WARNING, message, t, 2, FrameworkLogger.class);
    }

    /**
     * Just like FrameworkLogger.severe(String), but also logs a stack trace.
     *
     * @param t Throwable whose stack trace will be logged.
     * @see #severe(String)
     */
    public void severe(String message, Throwable t) {
        privlog(Level.SEVERE, message, t, 2, FrameworkLogger.class);
    }

    /**
     * Just like FrameworkLogger.info(String), but also logs a stack trace.
     *
     * @param t Throwable whose stack trace will be logged.
     * @see #info(String)
     */
    public void info(String message, Throwable t) {
        privlog(Level.INFO, message, t, 2, FrameworkLogger.class);
    }

    /**
     * Just like FrameworkLogger.finest(String), but also logs a stack trace.
     *
     * @param t Throwable whose stack trace will be logged.
     * @see #finest(String)
     */
    public void finest(String message, Throwable t) {
        privlog(Level.FINEST, message, t, 2, FrameworkLogger.class);
    }

    /**
     * Just like FrameworkLogger.error(String), but also logs a stack trace.
     *
     * @param t Throwable whose stack trace will be logged.
     * @see #error(String)
     */
    public void error(String message, Throwable t) {
        privlog(Level.WARNING, message, t, 2, FrameworkLogger.class);
    }

    /**
     * Whether this JVM is configured with java.util.logging defaults.
     *
     * If the JRE-provided config file is not in the expected place, then
     * we return false.
     */
    public static boolean isDefaultJdkConfig() {
        File globalCfgFile = new File(System.getProperty("java.home"),
                "lib/logging.properties");
        if (!globalCfgFile.isFile()) {
            return false;
        }
        FileInputStream fis = null;
        LogManager lm = LogManager.getLogManager();
        try {
            fis = new FileInputStream(globalCfgFile);
            Properties defaultProps = new Properties();
            defaultProps.load(fis);
            Enumeration names = defaultProps.propertyNames();
            int i = 0;
            String name;
            String liveVal;
            while (names.hasMoreElements()) {
                i++;
                name = (String) names.nextElement();
                liveVal = lm.getProperty(name);
                if (liveVal == null) {
                    return false;
                }
                if (!lm.getProperty(name).equals(liveVal)) {
                    return false;
                }
            }
            return true;
        } catch (IOException ioe) {
            return false;
        } finally {
            if (fis != null) try {
                fis.close();
            } catch (IOException ioe) {
                // Intentional no-op
            }
        }
    }
}
